C++ union 的一种妙用法

       union人皆知而鲜用的一个类。

       C++ 标准定义为:

1
A union is a class defined with the class-key union.

       我最近在写代码时,用到了如下实现:

1
2
3
4
5
6
7
8
9
10
11
template<typename T>
struct constant_init
{
union
{
T obj;
};
constexpr constant_init() : obj() {}
~constant_init() { /* do nothing, union object is not destroyed */}
};

       这是从标准库中学到的一种技巧。

       constant_init 用来在编译期创建一个类型 T 的对象 obj,obj 就放在了匿名的 union 中。

       为何要把一个对象放在 union 中?

       只因不想让该对象被销毁。

       union 便有此用,标准描述为:

1
2
Absent default member initializers, if any non-static data member of a union has a non-trivial default constructor, copy constructor, move constructor, copy assignment operator, move assignment operator, or destructor, the corresponding member function of the union will be implicitly deleted unless it is user-provided.
在没有默认成员初始化器的情况下,如果 union 的任一非静态数据成员具有非平凡的默认构造函数、拷贝构造函数、移动构造函数、拷贝赋值运行符、移动赋值运算符或析构函数,则其相应的成员函数将被隐式删除(除非用户显式定义)。

       例如:

1
2
3
4
5
union S
{
int a;
std::string str;
};

       S 无法使用,因为 std::string 导致它的对应成员函数被隐式删除了。

       可以通过以下方式来解决。

       一,使用 default member initializer:

1
2
3
4
5
6
7
8
union S
{
int a;
std::string str = "union str"; // default member initializer
~S() {} // explicitly define a destructor
} s;
std::cout << s.str;

       default member initializer 会构造一个 std::string 对象,于是成员 str 处于激活状态。此时,S 的默认构造函数不会被删除,但析构函数依旧被移除,需要显式写出(但 std::string 依旧不会被析构,必须显式调用析构)。

       二,聚合初始化:

1
2
3
4
5
6
7
8
union S
{
std::string str;
int a;
~S() {} // explicitly define a destructor
} s = { "union str"s };
std::cout << s.str;

       此时,S 的构造函数也被删除了。但是,聚合初始化能够初始化构造函数被删除的对象,聚合初始化的类本身就不允许有构造函数。
需要注意,union 的聚合初始化,默认初始化第一个成员,所以 str 被挪到最前面了。

       三,显式写出被删除的函数。这个就不必给例子了。

       介绍完关键知识,现在看回 constant_init。

       这个类的目的是,不想 T 对象被析构,union 的特性刚好满足此需求。

       为什么不想被析构?因为有些对象需要在程序的整个生命周期内,一直有效。

       最经典的一个例子就是标准中的 std::error_category,它用来为 std::error_code 指定域对象,std::error_code 的 operator== 通过域对象的地址来进行比较,所以域对象必须一直有效,也因此,它的实现依赖于单例模式。然而,单例模式也有析构顺序的问题,如果析构过早,后面又要使用该对象,需要令其“重生”。该问题我在 C++ DP.07 Singleton 中讲过,也讲过一种“重生”的技巧。constant_init 的实现也是为了规避此问题。

       然这只是其一,constant_init 的构造函数是 constexpr 的。这个小细节也十分必要,它是为了规避 SIOF(Static Initialization Order Fiasco) 问题。我在An In-depth Look at C++ Keyword: static 一文中也有详尽的描述。

       另外,正确使用 constant_init 也是极具细节的事。下面是一个正确使用的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
namespace
{
template<typename T>
struct constant_init
{
union
{
T obj;
};
constexpr constant_init() : obj() {}
~constant_init() { /* do nothing, union object is not destroyed */}
};
struct some_class {};
constant_init<some_class> some_class_instantance {};
} // anonymous namespace

       这段代码不应该放在头文件中,而应该放在实现文件中。

       这个原因,我在C++ Adventures: Namespaces 一文中也有详细描述。

       这也是此处使用 Unnamed(Anonymous) namespace 的原因。

       可见,这个技巧涉及的技术细节多如牛毛,任何一点不明白,就会误用。相比起来,union 倒是其中知识量最小的一个。

       读罢此文,相信大家能够掌握这一技巧。

文章目录